/*
 * Copyright (c) 2011 The Chromium OS Authors. All rights reserved.
 * Use of this source code is governed by a BSD-style license that can be
 * found in the LICENSE file.
 */

#include <ctype.h>
#include <errno.h>
#include <glib.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <syslog.h>

#include "mobile_provider.h"

struct mobile_provider_db {
  GHashTable *name2provider;
  GHashTable *network2provider;
};

struct parser_state {
  const gchar *filename;
  int linenum;
  GHashTable *name_table;
  GHashTable *network_table;
  gchar *country;
  struct mobile_provider *provider;
  struct mobile_apn *apn;
  int nameindex;
  int apnindex;
};

#define WARN(fmt, arg...)                                       \
    syslog(LOG_WARNING, "%s line %d: " fmt, state->filename,    \
           state->linenum, ## arg)
#ifdef DEBUG
#define DEBUG(fmt, arg...) printf(fmt, ## arg)
#else
#define DEBUG(fmt, arg...)
#endif

#define NORMAL_NETWORK_ID_LEN (6)

/************ Utility functions ************/

/*
 * Normalize a network ID by appending a '0' to the end if
 * the network ID consists of 5 digits. |output| is assumed
 * to refer to a buffer of at least 7 bytes.
 */
static void normalize(const gchar *input, gchar *output, size_t size)
{
  int len = strlen(input);
  if (len > size - 1)
    len = size - 1;
  strncpy(output, input, len);
  if (len == NORMAL_NETWORK_ID_LEN-1) {
    output[len] = '0';
    output[len+1] = '\0';
  } else {
    output[len] = '\0';
  }
}

/*
 * Normalize a list of network IDs. Each id that is changed
 * replaces the original string in the list.
 */
static void normalize_list(gchar **netids)
{
  int listlen = g_strv_length(netids);
  int i;

  for (i = 0; i < listlen; i++) {
    gchar *id = netids[i];
    if (strlen(netids[i])) {
      netids[i] = g_malloc0(NORMAL_NETWORK_ID_LEN+1);
      normalize(id, netids[i], NORMAL_NETWORK_ID_LEN+1);
      g_free(id);
    }
  }
}

/*
 * Extract a provider name from a string that may be of the form
 * "<provider-name> (<extra-text>)". Modifies its argument.
 */
static char *extract_name(gchar *text)
{
  char *paren = strchr(text, '(');

  if (paren != NULL) {
    *paren = '\0';
    g_strchomp(text);
  }
  return g_strdup(text);
}

static void free_network_providers(gpointer key, gpointer value,
                                   gpointer user_data)
{
  GSList *list = (GSList *)value;
  g_slist_free(list);
}

static void free_provider(gpointer key, gpointer value, gpointer user_data)
{
  struct mobile_provider *provider = (struct mobile_provider *)value;
  struct mobile_provider *next = provider->next;
  int i, j;

  /*
   * A provider may be in the hash table under multiple names.
   * Free the structure only when the last name is removed.
   */
  while (provider != NULL) {
    DEBUG("free_provider: %s / %s (%s)\n",
          key == NULL ? "***" : (char *)key,
          provider->names[0]->name, provider->country);
    next = provider->next;
    if (--provider->refcnt != 0) {
      DEBUG("Not freeing %s / %s, refcnt = %d\n",
            key == NULL ? "***" : (char *)key,
            provider->names[0]->name,
            provider->refcnt);
      break;
    }
    for (i = 0; i < provider->num_apns; i++) {
      for (j = 0; j < provider->apns[i]->num_names; j++) {
        g_free(provider->apns[i]->names[j]->name);
        g_free(provider->apns[i]->names[j]);
      }
      g_free(provider->apns[i]->names);
      g_free(provider->apns[i]->value);
      g_free(provider->apns[i]->username);
      g_free(provider->apns[i]->password);
      g_free(provider->apns[i]);
    }
    g_free(provider->apns);
    for (i = 0; i < provider->num_names; i++) {
      g_free(provider->names[i]->name);
      g_free(provider->names[i]->lang);
      g_free(provider->names[i]);
    }
    g_free(provider->names);
    g_strfreev(provider->networks);
    g_free(provider);
    /*
     * key is invalid after provider is freed
     */
    key = NULL;
    provider = next;
  }
}

static void provider_iter_shim(gpointer key, gpointer value, gpointer user_data)
{
  struct mobile_provider *provider = (struct mobile_provider *)value;
  ProviderIterFunc func = (ProviderIterFunc)user_data;
  func(provider);
}

static void network_iter_shim(gpointer key, gpointer value, gpointer user_data)
{
  gchar *network = (gchar *)key;
  struct mobile_provider *provider = (struct mobile_provider *)value;
  NetworkIterFunc func = (NetworkIterFunc)user_data;
  func(network, provider);
}

static void handle_provider(struct parser_state *state, gchar *text)
{
  gchar **pfields;
  int num_names;
  int num_apns;
  int primary;
  int roaming;
  struct mobile_provider *provider;

  state->provider = NULL;
  state->apn = NULL;
  state->apnindex = 0;
  state->nameindex = 0;
  pfields = g_strsplit(text, ",", 0);
  if (g_strv_length(pfields) != 4) {
    WARN("Badly formed \"providers\" entry: \"%s\"", text);
    g_strfreev(pfields);
    return;
  }
  errno = 0;
  num_names = strtol(pfields[0], NULL, 0);
  num_apns = strtol(pfields[1], NULL, 0);
  primary = strtol(pfields[2], NULL, 0);
  roaming = strtol(pfields[3], NULL, 0);
  if (errno != 0) {
    WARN("Error parsing \"providers\" entry \"%s\"", text);
    g_strfreev(pfields);
    return;
  }
  provider = g_new0(struct mobile_provider, 1);
  strncpy(provider->country, state->country, sizeof(provider->country)-1);
  provider->num_names = num_names;
  if (num_names != 0)
    provider->names = g_new0(struct localized_name *, num_names);
  provider->num_apns = num_apns;
  provider->primary = primary != 0;
  if (num_apns != 0)
    provider->apns = g_new0(struct mobile_apn *, num_apns);
  provider->requires_roaming = roaming;
  state->provider = provider;
  g_strfreev(pfields);
}

static void network_add_provider(GHashTable *network_table,
                                 gchar *network_id,
                                 struct mobile_provider *provider)
{
  GSList *list;

  list = g_hash_table_lookup(network_table, network_id);
  list = g_slist_prepend(list, provider);
  g_hash_table_insert(network_table, network_id, list);
}

static void handle_networks(struct parser_state *state, gchar *text)
{
  int listlen;
  int i;
  struct mobile_provider *provider;

  if (state->provider == NULL)
    return;

  provider = state->provider;
  provider->networks = g_strsplit(text, ",", 0);
  if (provider->networks != NULL) {
    normalize_list(provider->networks);
    listlen = g_strv_length(provider->networks);
    for (i = 0; i < listlen; i++) {
      network_add_provider(state->network_table,
                           provider->networks[i],
                           provider);
    }
  }
}

static void handle_apn(struct parser_state *state, gchar *text)
{
  gchar **apnfields;
  struct mobile_apn *apn;
  struct mobile_provider *provider;
  int num_names;

  if (state->provider == NULL)
    return;

  state->apn = NULL;
  provider = state->provider;
  state->nameindex = 0;
  apnfields = g_strsplit(text, ",", 0);
  if (g_strv_length(apnfields) != 4) {
    WARN("Badly formed \"apn\" entry: \"%s\"", text);
    g_strfreev(apnfields);
    return;
  }
  errno = 0;
  num_names = strtol(apnfields[0], NULL, 0);
  if (errno != 0) {
    WARN("Error parsing \"apn\" entry \"%s\"", text);
    g_strfreev(apnfields);
    return;
  }
  apn = g_new0(struct mobile_apn, 1);
  apn->num_names = num_names;
  apn->value = g_strdup(apnfields[1]);
  if (strlen(apnfields[2]) != 0)
    apn->username = g_strdup(apnfields[2]);
  if (strlen(apnfields[3]) != 0)
    apn->password = g_strdup(apnfields[3]);
  provider->apns[state->apnindex++] = apn;
  state->apn = apn;
  g_strfreev(apnfields);
}

static void handle_name(struct parser_state *state, gchar *text)
{
  gchar **namefields;
  struct localized_name *name = NULL;
  struct mobile_provider *provider;
  struct mobile_provider *other_provider;
  struct mobile_apn *apn;

  if (state->provider == NULL && state->apn == NULL)
    return;

  provider = state->provider;
  apn = state->apn;
  namefields = g_strsplit(text, ",", 2);
  if (g_strv_length(namefields) != 2) {
    WARN("Badly formed \"name\" entry: \"%s\"", text);
    g_strfreev(namefields);
    return;
  }
  name = g_new0(struct localized_name, 1);
  if (strlen(namefields[0]) != 0)
    name->lang = g_strdup(namefields[0]);

  if (apn != NULL) {
    if (apn->names == NULL)
      apn->names = g_new0(struct localized_name *,
                          apn->num_names);
    name->name = g_strdup(namefields[1]);
    apn->names[state->nameindex++] = name;
  } else if (provider != NULL) {
    /*
     * If this is the first name encountered for the provider,
     * put the provider in the hash table under this name.
     */
    if (state->nameindex == 0) {
      name->name = extract_name(namefields[1]);
      /*
       * If a provider already exists under this name, but
       * for a different country, then chain the new provider
       * onto the end of a linked list.
       */
      other_provider = g_hash_table_lookup(state->name_table,
                                           name->name);
      if (other_provider == NULL) {
        DEBUG("Add new %s (%s)\n", name->name, provider->country);
        g_hash_table_insert(state->name_table,
                            name->name, provider);
      } else {
        DEBUG("Chain %s (%s) to (%s)\n", name->name,
              provider->country,
              other_provider->country);
        while (other_provider->next != NULL)
          other_provider = other_provider->next;
        other_provider->next = provider;
      }
      ++provider->refcnt;
    } else {
      name->name = g_strdup(namefields[1]);
      DEBUG("Additional name %s (%s) for %s\n",
            name->name, provider->country,
            provider->names[0]->name);
      if (g_hash_table_lookup(state->name_table,
                              name->name) == NULL) {
        g_hash_table_insert(state->name_table,
                            name->name, provider);
        ++provider->refcnt;
      }
    }
    provider->names[state->nameindex++] = name;
  }
  g_strfreev(namefields);
}

/*********** Exported functions ***********/

struct mobile_provider_db *mobile_provider_open_db(const gchar *pathname)
{
  char linebuf[BUFSIZ];
  char *line;
  size_t linelen;
  gchar **fields;
  FILE *dbfile;
  struct mobile_provider_db *db = NULL;
  int firstline = 1;
  const gchar *fname;
  struct parser_state *state;

  dbfile = fopen(pathname, "r");
  if (dbfile == NULL)
    return NULL;

  fname = strrchr(pathname, '/');
  if (fname == NULL)
    fname = pathname;
  else
    ++fname;
  state = g_new0(struct parser_state, 1);
  state->filename = fname;
  while ((line = fgets(linebuf, sizeof(linebuf), dbfile)) != NULL) {
    ++state->linenum;
    linelen = strlen(line);
    if (linelen <= 1 || line[0] == '#')
      continue;
    if (line[linelen-1] == '\n')
      line[linelen-1] = '\0';
    fields = g_strsplit(line, ":", 2);
    if (g_strv_length(fields) != 2) {
      WARN("Badly formed line: \"%s\"", line);
      g_strfreev(fields);
      continue;
    }
    /* Do a basic check to see whether we have a valid file */
    if (firstline) {
      char *errmsg = NULL;
      if (strcmp(fields[0], "serviceproviders") != 0)
        errmsg = "File does not begin with \"serviceproviders\" entry";
      else if (strcmp(fields[1], "2.0") != 0)
        errmsg = "Unrecognized serviceproviders format";
      if (errmsg != NULL) {
        WARN("%s", errmsg);
        g_strfreev(fields);
        g_free(state);
        errno = ENOSYS;
        fclose(dbfile);
        return NULL;
      }
      firstline = 0;
      state->name_table = g_hash_table_new(g_str_hash, g_str_equal);
      state->network_table = g_hash_table_new(g_str_hash, g_str_equal);
    } else if (strcmp(fields[0], "country") == 0) {
      g_free(state->country);
      state->country = g_strdup(fields[1]);
    } else if (strcmp(fields[0], "provider") == 0) {
      handle_provider(state, fields[1]);
    } else if (strcmp(fields[0], "networks") == 0) {
      handle_networks(state, fields[1]);
    } else if (strcmp(fields[0], "apn") == 0) {
      handle_apn(state, fields[1]);
    } else if (strcmp(fields[0], "name") == 0) {
      handle_name(state, fields[1]);
    }
    g_strfreev(fields);
  }
  fclose(dbfile);
  if (state->country == NULL && state->provider == NULL) {
    g_hash_table_destroy(state->name_table);
    g_hash_table_destroy(state->network_table);
    errno = ENOMSG;
  } else {
    g_free(state->country);
    db = g_new0(struct mobile_provider_db, 1);
    db->name2provider = state->name_table;
    db->network2provider = state->network_table;
  }
  g_free(state);
  return db;
}

void mobile_provider_close_db(struct mobile_provider_db *db)
{
  if (db != NULL) {
    g_hash_table_foreach(db->name2provider, free_provider, NULL);
    g_hash_table_destroy(db->name2provider);
    g_hash_table_foreach(db->network2provider, free_network_providers, NULL);
    g_hash_table_destroy(db->network2provider);
    g_free(db);
  }
}


struct mobile_provider *network_find_provider(
    const struct mobile_provider_db *db,
    const gchar *network_id,
    gboolean primary)
{
  GSList *list;

  if (db == NULL || network_id == NULL)
    return NULL;

  list = g_hash_table_lookup(db->network2provider, network_id);
  for( ; list != NULL; list = list->next) {
    struct mobile_provider *provider = list->data;
    if (provider->primary == primary)
      return provider;
  }
  return NULL;
}

struct mobile_provider *mobile_provider_lookup_by_network(
    const struct mobile_provider_db *db,
    const gchar *network_id)
{
  char netid[NORMAL_NETWORK_ID_LEN+1];
  struct mobile_provider *provider;
  int primary;

  if (db == NULL || network_id == NULL)
    return NULL;

  /*
   * First try to find a match of a primary provider, if that
   * fails, return the first matching non-primary provider.
   */
  for (primary = 1; primary >= 0; primary--) {

    /*
     * Try the lookup of the MCC/MNC pair with 6 digits
     * and then 5 digits, to account for the vagaries of
     * whether the MNC is 2 or 3 digits, which is
     * difficult to determine.
     */
    normalize(network_id, netid, sizeof(netid));
    provider = network_find_provider(db, netid, primary);
    if (provider != NULL)
      return provider;
    normalize(network_id, netid, sizeof(netid) - 1);
    provider = network_find_provider(db, netid, primary);
    if (provider != NULL)
      return provider;
  }
  return NULL;
}

struct mobile_provider *mobile_provider_lookup_by_name(
    const struct mobile_provider_db *db,
    const gchar *provider_name)
{
  if (db != NULL && provider_name != NULL && strlen(provider_name) != 0)
    return g_hash_table_lookup(db->name2provider, provider_name);
  return NULL;
}

struct mobile_provider *mobile_provider_lookup_best_match(
    const struct mobile_provider_db *db,
    const gchar *provider_name,
    const gchar *network_id)
{
  struct mobile_provider *provider = NULL;

  if (db == NULL)
    return NULL;

  if (provider_name != NULL)
    provider = g_hash_table_lookup(db->name2provider,
                                   provider_name);
  /*
   * We have a linked list of providers with the given name, but
   * they may not be in the right country. Pick one with a
   * matching MCC. If there are no networks listed for the provider,
   * and there are no other providers with the same name, then return
   * the result without doing any further checks.
   */
  if (provider != NULL) {
    /*
     * If the provider has no networks listed, then return it
     * if it is unambiguous, i.e., there's only one entry for
     * that provider name.
     */
    if (provider->networks == NULL || provider->networks[0] == NULL) {
      if (provider->next == NULL)
        return provider;
      else {
        syslog(LOG_WARNING,
               "%s: Provider \"%s\" in country \"%s\" has no networks, "
               "but has multiple entries.",
               __func__, provider_name, provider->country);
        return NULL;
      }
    } else {
      for ( ; provider != NULL ; provider = provider->next) {
        gchar **networks;
        for (networks = provider->networks;
             *networks != NULL;
             networks++) {
          if (strncmp(network_id, *networks, 3) == 0)
            break;
        }
        if (*networks != NULL)
          break;
      }
    }
  }
  if (provider ==NULL)
    provider = mobile_provider_lookup_by_network(db, network_id);
  return provider;
}


const gchar *mobile_provider_get_name(struct mobile_provider *provider)
{
  if (provider != NULL && provider->num_names != 0)
    return provider->names[0]->name;
  return NULL;
}

void mobile_provider_foreach_provider(const struct mobile_provider_db *db,
                                      ProviderIterFunc func)
{
  g_hash_table_foreach(db->name2provider, provider_iter_shim, func);
}

void mobile_provider_foreach_network(const struct mobile_provider_db *db,
                                     NetworkIterFunc func)
{
  g_hash_table_foreach(db->network2provider, network_iter_shim, func);
}
